%  This file is part of Erlywog.

%  Erlywog is free software: you can redistribute it and/or modify
%  it under the terms of the GNU General Public License as published by
%  the Free Software Foundation, either version 3 of the License, or
%  (at your option) any later version.

%  Erlywog is distributed in the hope that it will be useful,
%  but WITHOUT ANY WARRANTY; without even the implied warranty of
%  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%  GNU General Public License for more details.

%  You should have received a copy of the GNU General Public License
%  along with Erlywog.  If not, see <http://www.gnu.org/licenses/>.

%  Copyright Jasm Sison 2009

-module(login_controller).
-include_lib("eunit/include/eunit.hrl").
-export([
	index/1
	, ajax_send_salts/1
	, process_login/1
	, md5jQuery/1
	, verify_login/3
	, md5_hex/1
	, cookie_set_test_/1 %% test
	, cookietime/1 %% test
	]).

% TODO
% - test case: if cookie not there, but user is assigned a session key
% - test case: if cookie is there, but user is not assigned a session key
% - test case: if cookie is there, but user is not assigned the same session key
% - test case: if cookie has expired, then session key must not linger on

%% The default component to return if only the module is called % success
index (A) ->
  Ewcs = [ {data, integer_to_list(key_manager:key_size())} ]
  , PhasedVars = [
      {html_header_title, <<"login">>}
      , {html_header, {ewc, login, md5jQuery, [A]}}
  ]
  , blog_util:phased_response(Ewcs, PhasedVars).
  
md5jQuery(_A) ->
  {data, []}.

%% misdirection in case someone is busy with an enumeration attack via ajax % success

% make_string(N) ->
%   base64:encode(lists:map(fun (_) -> random:uniform(90)+$\s+1 end, lists:seq(1,N))).

%% Succesful, but crashes yaws, so useless after all.
% fake_salt () ->
%   N = key_manager:key_size() %% Take a hint from the key_manager
%   , {data, make_string(N)}.

%% provide the salt when given the right username, or give misdirection
ajax_send_salts(A) ->
  %% BUG solved + weakness is if they try the same challenge twice and find out the static is not very static at all...
  %% TODO + build fake static salt repository...
  Misdirection = fun(LoginPhrase_) ->
    %% check if the user was already issued a static key
    case key_manager:get_fake_salt(LoginPhrase_) of
      not_found -> %% not found
        print("No fake salt issued yet"),
        [ {data, key_manager:issue_fake_salt(LoginPhrase_)}, {data, key_manager:issue_dynamic_salt(LoginPhrase_)} ]
        
      ;Salt  -> %% else, issue a static key
        print("Fake salt already issued"),
        [ {data, Salt}, {data, key_manager:issue_dynamic_salt(LoginPhrase_)} ]
        
    end
  end, %% RISK: opens up DOS attack for ETS overflow, let's hope you have enough memory.
  FindPhraseInDB = fun(LoginPhrase_) ->
    case blogger:find_first({login_phrase,'=', LoginPhrase_}) of
      undefined -> Misdirection(LoginPhrase_);    %% Phrase not found in the DB, give misdirection
      Blogger -> [ {data, blogger:salt(Blogger)}, {data, key_manager:issue_dynamic_salt(LoginPhrase_)} ]  %% send static salt, dynamic salt %% old code works with ajax component, deprecated
%         Blogger -> json:encode({array, [blogger:salt(Blogger), key_manager:issue_dynamic_salt(LoginPhrase_)]}) %% broken!
%       Blogger -> {response, [{html, json:encode({array, [blogger:salt(Blogger), key_manager:issue_dynamic_salt(LoginPhrase_)]})}]} %% broken!
    end
  end,
  case yaws_arg:method(A) of
   'GET' ->
     {ewr, login};
   'POST' ->
      Output =
 	case proplists:get_value("login_phrase", yaws_api:parse_post(A)) of
		undefined -> [{data,<<"Please">>},{data,<<"FuckOff">>}];    %% Something else was sent in place of a 'login_phrase' input, give misdirection
		LoginPhrase -> FindPhraseInDB(LoginPhrase) %% Look for the damn user
	end
 	, blog_util:ajax_escape(Output)
  end.

%% process login attempt
process_login (A) ->
  case yaws_arg:method(A) of
   'GET' ->
     {ewr, login}; %% use anything other than post, you will be redirected to the login
   'POST' ->
      case erlyweb_forms:validate(A, ["login_phrase","hash"], fun validate_field/2) of
	{[LoginPhrase, Hash], []} ->
	  [{ewc, login, verify_login, [A, LoginPhrase, Hash]}];
        {Inputs, Errors} ->
          {data, {Inputs, Errors}}
%         ;Otherwise -> io:format("~p~n", [Otherwise])
      end
  end.

%% validators
validate_field("login_phrase", Val) when length(Val) > 4 -> ok;
validate_field("login_phrase", _) -> {value_too_short, "login_phrase", "8"};
validate_field("hash", Val) when length(Val) > 20 -> ok;
validate_field("hash", _) -> {value_too_short, "hash", "25"}.

%% constant for process_login/1
cookie_valid_secs() -> 10800. %% 3 hours * 3600 seconds
cookie_valid_redirect() -> {ewr, blog}.
cookie_valid_path(A) -> erlyweb:get_app_root(A).

cookie_time_add(Datetime, AddedSecs) ->
        NowSecs = calendar:datetime_to_gregorian_seconds(Datetime)
      , FutureSecs = NowSecs + AddedSecs
      , {Date,Time} = calendar:gregorian_seconds_to_datetime(FutureSecs)
      , {datetime, Date, Time}.
      
md5_hex(S) ->
  md5:hex(S).

%% TODO - do not send phrase, but send static key instead, issue dynamic key to static key (instead of user identifier)

verify_login(A, LoginPhrase, Hash) ->
%   Error = fun(LoginPhrase_) -> {data, {LoginPhrase_, [invalid_credentials]}} end,
  Error = fun(Output_) -> {data, Output_} end,
  ErrorMessage = <<"<br/>Please <a href='/login'>try again</a>">>,
  case key_manager:get_dynamic_salt(LoginPhrase) of
    not_found ->
      Error(ErrorMessage); %% no dynamic salt found
    DynamicSalt ->
      case blogger:find_first({login_phrase,'=',LoginPhrase}) of
        undefined ->
          Error(ErrorMessage); %% dynamic salt found but user does not exist! (This will never happen)
        Blogger ->
          ServerHash = md5_hex(erlang:concat_binary([blogger:hash(Blogger),  DynamicSalt])),
          case ServerHash =:= list_to_binary(Hash) of
            false ->
%               Error([<<"\n<br/>client hash: ">>, Hash, <<"\n<br/>server hash: ">>, ServerHash, ErrorMessage]);
		%% user and dynamic salt found, but bad hash identification -> wrong password.
              Error(ErrorMessage);
            true ->
              ActiveSessionKey = key_manager:issue_session_key(LoginPhrase)
              , Cookies = [
                yaws_api:setcookie("hash", blogger:hash(Blogger), "/")  %% set 
                , yaws_api:setcookie("active_session_key", ActiveSessionKey, "/")
              ]
		, BloggerName = blogger:name(Blogger)
		, WelcomeTitle = [{html_header_title, [<<"Welcome ">>, BloggerName, <<"!">>]}]
% 		, blog_util:phased_response([{ewr,blog}], WelcomeTitle ++ Cookies) %% broken
 		, {response, lists:flatten([Cookies, {body, {ewc, blog}}])} %% might work...
% 		, {response, {body, {ewr, blog}}} %% might also work, try on non-broken erlyweb?
          end
      end
  end.
  
  
%% tests!
cookietime(A) ->
  %blog_util:phased_response([{data, "test1"},{data, "test2"}], [{html_header_title, <<"shit!">>}]). %% works!

%   Cookies = {header, [yaws_api:setcookie("fuckface", <<"shiznit">>,"/"),yaws_api:setcookie("assmunch","dickformuff")]},
%   blog_util:phased_response([{data, <<"test">>},{data, " fucking"}] ,[{html_header_title, <<"shit!">>},Cookies]). %% works

%   {response, [{header, [yaws_api:setcookie("fuck","face")]},{html, <<"Motherfucker">>}]}. %% breaks yaws!
  {data, <<"shit">>}.
  

cookie_set_test_(A) ->
  Hash = yaws_api:find_cookie_val("hash", yaws_headers:cookie(A)),
  ASK = yaws_api:find_cookie_val("active_session_key", yaws_headers:cookie(A)),
  case Hash =:= ASK of
    true -> %% both undefined, or have identical value
      Cookies = [
        yaws_api:setcookie("hash", "haaaaash", "/")  %% set 
        , yaws_api:setcookie("active_session_key", "Keeeeeeeeeeeeeeeeeeeeeeey", "/")
      ],
      blog_util:phased_response([{data, "Has the cookie been set? No."}, {data, [<<"<br/>">>, Hash]}, {data, [<<"<br/>">>, ASK]}], Cookies);
    false -> %% at least one undefined
      [{data, [<<"<br/>hash contains: ">>, Hash]},{data, [<<"<br/>active_session_key contains: ">>, ASK]}]
  end.

md5_hex_test_() ->
  Blogger = blogger:find_first({login_phrase,'=',<<"Soundgarden">>})
  , DynamicSalt = <<"abcd">>
  , print(md5_hex(<<"SoundgardenaFell on black days">>))
  , print(blogger:hash(Blogger))
  , Check = md5_hex(erlang:concat_binary([md5_hex(<<"SoundgardenaFell on black days">>), DynamicSalt]))
  , CounterCheck = md5_hex(erlang:concat_binary([blogger:hash(Blogger),  DynamicSalt]))
  , Blogger1 = blogger:find_first({login_phrase,'=',<<"The Mars Volta">>})
  , DynamicSalt1 = <<"abcd">>
  , print(md5_hex(<<"The Mars VoltabOurobouros">>))
  , print(blogger:hash(Blogger1))
  , Check1 = md5_hex(
  	erlang:concat_binary(
  		[md5_hex(<<"The Mars VoltabOurobouros">>), DynamicSalt1])
  )
  , CounterCheck1 = md5_hex(erlang:concat_binary([blogger:hash(Blogger),  DynamicSalt1]))
  , ?assert(Check =:= CounterCheck)
  , ?assert(Check1 =:= CounterCheck1).

print(Str) ->
   io:format("-------------------------------~s--------------------------------------~n",[Str]).
